<?php

namespace liketp\foundation;

use Closure;
use Throwable;
use liketp\exception\Exception;

class Pipeline
{
    protected $assembleType; // 'onion' | 'cowpea'

    protected $passData;
    protected $handleList;

    protected $exceptionHandle;

    protected $fnKey = 'fn';
    protected $argsKey = 'args';

    /**
     * $type string  onion | cowpea
     */
    public function __construct(string $assembleType = 'onion', ?string $fnKey = null, ?string $argsKey = null)
    {
        $this->type($assembleType);
        if ($fnKey !== null) $this->fnKey = $fnKey;
        if ($argsKey !== null) $this->argsKey = $argsKey;
    }

    public function send($passData)
    {
        $this->passData = $passData;
        return $this;
    }

    public function through(array $handleList)
    {
        $this->handleList = $handleList;
        return $this;
    }

    public function then(Closure $finalFn)
    {
        $assembleFn = $this->assembleHandleList($finalFn);

        return $assembleFn($this->passData);
    }

    public function whenException($handle)
    {
        $this->exceptionHandle = $handle;
        return $this;
    }

    public function type(?string $type = null)
    {
        if (is_null($type)) {
            return $this->assembleType;
        } else {
            if (in_array($type, ['onion', 'cowpea']) == false) {
                throw new \Exception('参数有误');
            }
            $this->assembleType = $type;
            return $this;
        }
    }

    //------------------------------------------------ protected
    protected function assembleHandleList(Closure $finalFn): Closure
    {
        $finalFn = $this->wrapFinalFn($finalFn);

        if ($this->assembleType == 'onion') {
            $assembleFn = $this->assembleToOnion($finalFn);
        } else if ($this->assembleType == 'cowpea') {
            $assembleFn = $this->assembleToCowpea($finalFn);
        }
        return $assembleFn;
    }

    protected function wrapFinalFn(Closure $finalFn): Closure
    {
        return function ($passData) use ($finalFn) {
            try {
                return $finalFn($passData);
            } catch (Throwable $e) {
                return $this->handleException($passData, $e);
            }
        };
    }

    protected function handleException($passData, $err)
    {
        if (is_callable($this->exceptionHandle)) {
            return ($this->exceptionHandle)($passData, $err);
        } else {
            throw $err;
        }
    }

    //------------------------------------------------ private
    private function assembleToOnion(Closure $lastFn): Closure
    {
        $handleList = array_reverse($this->handleList);
        $reduceHandle = $this->getOnionReduceHandle();
        $assembleFn = array_reduce($handleList, $reduceHandle, $lastFn);
        return $assembleFn;
    }

    private function getOnionReduceHandle(): Closure
    {
        return function ($stack, $wareItem) {
            return function ($passData) use ($stack, $wareItem) {
                $fn = $this->getWareFn($wareItem);
                $args = $this->getWareArgs($wareItem);
                return $fn($stack, $passData, $args);
            };
        };
    }

    private function assembleToCowpea(Closure $lastFn): Closure
    {
        $handleList = $this->handleList;
        $beforeList = array_column($handleList, 'before');
        $afterList = array_column($handleList, 'after');
        $afterList = array_reverse($afterList);

        $assembleFn = function ($passData) use ($beforeList, $afterList, $lastFn) {
            foreach ($beforeList as $wareItem) {
                $fn = $this->getWareFn($wareItem);
                $args = $this->getWareArgs($wareItem);
                $passData = $fn($passData, $args);
            }

            $result = $lastFn($passData);

            foreach ($afterList as $wareItem) {
                $fn = $this->getWareFn($wareItem);
                $args = $this->getWareArgs($wareItem);
                $result = $fn($result, $args);
            }

            return $result;
        };

        return $assembleFn;
    }

    private function getWareFn($ware)
    {
        return is_array($ware) ? $ware[$this->fnKey] : $ware;
    }

    private function getWareArgs($ware)
    {
        return is_array($ware) ? ($ware[$this->argsKey] ?? []) : [];
    }
}
